<?php 
/**
 * @name 微信支付PHPSDK，实现了微信支付大部分接口  
 * @author crazymus < QQ:291445576 >
 * @version 2.0.1
 * @licensed apache2.0
 * @updated 2016-01-19
 * @link https://pay.weixin.qq.com/wiki/doc/api/index.html
 */

namespace Org\Util; 
class WechatPay{

	protected static $config = array(
		//app id
		'appid' => '',
		//app key
		'app_key' => '',
		//商户号
		'mch_id' => '',
		//密钥 
		'appsecret' => '',
		//证书放在非WEB目录中 保证安全   
        'SSLCERT_PATH' => '',
        'SSLKEY_PATH' => ''
	);
	
	/**
	 * @name 动态配置 
	 * @param array 
	 */
	public function init($config){
		self::$config = array_merge(self::$config, $config);
	}
	
	/**
	 * @name 读取配置信息 
	 * @return array 
	 */
	public function getConfig(){
		return self::$config;
	}

	/**
	 * @name 统一下单 
	 * @param array 
	 * array(
		'body' => '订单描述',
		'out_trade_no' => '订单号',
		'total_fee' => '金额 单位为分',
		'notify_url' => '接收支付信息地址', //必须使用pathinfo，不可带query_string 
		'trade_type' => '支付类别 JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付',
		'openid' => '用户openid 支付类别为JSAPI必须提供'
	 )
	 * @return JSON  若交易类型为JSAPI，则返回
	 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
	 需要的参数 
	 若交易类型为APP，则返回
	 https://pay.weixin.qq.com/wiki/doc/api/app.php?chapter=9_12&index=2
	 需要的参数 
	 */
	public static function UnifiedOrder($arr) {
		$arr = array(
			'appid' => self::$config['appid'],
			'mch_id' => self::$config['mch_id'],
			'nonce_str' => self::makeNonceStr(),
			'body' => $arr['body'],
			'out_trade_no' => $arr['out_trade_no'],
			'total_fee' => $arr['total_fee'],
			'spbill_create_ip' => $_SERVER['REMOTE_ADDR'],
			'notify_url' => $arr['notify_url'],
			'trade_type' => $arr['trade_type'],
			'openid' => $arr['openid']
		);
		
		//生成签名  
		$arr['sign'] = self::makeSign($arr);
		$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
		$result = self::post($url, $arr);

		if($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS' ){
			$output = array(
				'success' => true,
				'msg' => '成功获取prepay_id'
			);
			
			if($arr['trade_type'] == 'JSAPI'){
				//网页调起微信支付的返回值 
				$params = array(
					'appId' => self::$config['appid'],
					'timeStamp' => time(),
					'nonceStr' => self::makeNonceStr(),
					//prepay_id
					'package' => "prepay_id=".$result['prepay_id'],
					//签名方式
					'signType' => 'MD5'
				);
				//计算签名 
				$params['paySign'] = self::makeSign($params);
				
			}else if($arr['trade_type'] == 'APP'){
				//APP调起微信
				$params = array(
					'appid' => self::$config['appid'],
					'partnerid' => self::$config['mch_id'],
					'prepayid' => $result['prepay_id'],
					'package' => 'Sign=WXPay',
					'noncestr' => self::makeNonceStr(),
					'timestamp' => time()
				);
				//计算签名 
				$params['sign'] = self::makeSign($params);
			}else{
				$params = '抱歉，当前仅支持JSAPI和APP支付';
			}
			
			$output['params'] = $params;
			
		}else{
			$output = array(
				'success' => false,
				'msg' => '获取prepay_id失败, 请重新尝试'
			);
		}
		
		return $output; 
	}
	
	/**
	 * @name 查询订单
	 * @param array 传入订单号(out_trade_no) 或微信订单号(transaction_id)
	 * @param callbackContext 回调类 实现WechatPayQuery接口  
	 * @output JSON 查询及回调方法执行结果 
	 */
	public static function queryOrder($array, WechatPayQuery $callbackContext){
	
		//获取配置信息 
		$config = self::$config;
		
		//获取appid和商户号
		$array['appid'] = $config['appid'];
		$array['mch_id'] = $config['mch_id'];
		
		//生成随机字符串
		$array['nonce_str'] = self::makeNonceStr();
		
		//生成签名
		$sign = self::makeSign($array);
		$array['sign'] = $sign;
		
		$url = 'https://api.mch.weixin.qq.com/pay/orderquery';
		$result = self::post($url, $array);
				
		//=========校验签名==========//
		$sign = self::makeSign($result);
		if($sign != $result['sign']){	
			echo json_encode(array(
				'success' => false,
				'msg' => '签名校验失败'
			));
		}
		
		if($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS' ){
			//订单支付成功 执行回调 
			$callback_result = $callbackContext->success($result);
			if($callback_result){
				$output = array(
					'success' => true,
					'msg' => '订单处理成功'
				);
			}else{
				$output = array(
					'success' => false,
					'msg' => '订单处理失败'
				);
			}
		}else{
			//查询失败的返回值 
			$output = array(
				'success' => false,
				'msg' => '订单查询失败, 请重新尝试'
			);
		}
		
		return $output;
	}
	
	/**
	 * @name 接收微信回传信息, 并完成业务逻辑
	 * @param $callbackContext 回调类，实现WechatPayNotify接口
	 * @output XML 参见https://pay.weixin.qq.com/wiki/doc/api/app.php?chapter=9_7&index=3
	 */
	public static function notify(WechatPayNotify $callbackContext){
		//======获取原生的POST数据===========//
		$xml = file_get_contents("php://input");
		if(!$xml){
			return null;
		}
		//转成数组
		$array = self::toArray($xml);
		//========生成签名===========//
		$sign = self::makeSign($array);
		//=========校验签名========//
		if($sign != $array['sign']){
			$result = array(
				'return_code' => 'FAIL',
				'return_msg' => '签名校验失败'
			);
			echo self::toXml($result);
			exit; 
		}
		
		if($array['result_code'] == 'SUCCESS'){
			//表示支付成功 执行业务逻辑 
			$callback_result = $callbackContext->success($array);
			if($callback_result){
				$result = array(
					'return_code' => 'SUCCESS',
					'return_msg' => '订单处理成功'
				);
				
				echo self::toXml($result);
			}
		}
		
	}
	
	/**
	 * @name 申请退款 
	 * @param array(
			'out_trade_no' => '商户订单号', 
			'out_refund_no' => '退款订单号',
			'total_fee' => '总费用',
			'refund_fee' => '退款金额'
		)
	   @param WechatPayRefund 申请退款成功后执行的回调 
	   @return array 
	   成功:
	   array(
			'success' => true,
			'msg' => '成功的提示'
	   )
	   失败:
	   array(
			'success' => false,
			'msg' => '失败的提示'
	   )
	 */
	public function refund($params, WechatPayRefund $callbackContext){
		//退款地址 
		$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
		//获取配置信息  
		$config = self::$config;
			
		$array['appid'] = $config['appid'];
		$array['mch_id'] = $config['mch_id'];
		$array['nonce_str'] = self::makeNonceStr();
		$array['out_trade_no'] = $params['out_trade_no'];
		$array['out_refund_no'] = $params['out_refund_no'];
		//总费用 
		$array['total_fee'] = $params['total_fee'];
		//退款金额 
		$array['refund_fee'] = $params['refund_fee'];
		//操作员 默认为商户号 
		$array['op_user_id'] = $config['mch_id'];
		//生成签名 
		$array['sign'] = self::makeSign($array);
		
		$result = self::postSSL($url, $array);
		if($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS'){
			$callback_result = $callbackContext->success($result);
			if($callback_result){
				$output = array(
					'success' => true,
					'msg' => '申请退款成功, 请稍后查询退款结果'
				);
			}else{
				$output = array(
					'success' => false,
					'msg' => '申请退款失败, 请重新尝试'
				);
			}
		}else{
			$output = array(
				'success' => false,
				'msg' => '申请退款失败, 请重新尝试'
			);
		}
		
		return $output;
	
	}
	
	/**
	 * @name 查询退款结果 
	 * @param string 商户订单号 
	 * @param WechatPayRefundQuery 退款成功后的回调
	 * @return array 
	   成功:array(
			'success' => true,
			'msg' => '成功提示'
	   )
	   失败:array(
			'success' => false,
			'msg' => '失败提示'
	   )
	 */
	public function refundQuery($out_trade_no, WechatPayRefundQuery $callbackContext){
		
		$url = 'https://api.mch.weixin.qq.com/pay/refundquery';
		
		$config = self::$config;
		$array['appid'] = $config['appid']; 
		$array['mch_id'] = $config['mch_id'];
		$array['nonce_str'] = self::makeNonceStr();
		$array['out_trade_no'] = $out_trade_no;
		
		$array['sign'] = self::makeSign($array);
		
		$result = self::post($url, $array);
		if($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS' ){
			if($result['refund_status_$n'] == 'SUCCESS'){
				$callback_result = $callbackContext->success($result);
				if($callback_result){
					$output = array(
						'success' => true,
						'msg' => '退款成功'
					);
				}else{
					$output = array(
						'success' => false,
						'msg' => '业务处理失败, 请重新尝试'
					);
				}
				
			}else if($result['refund_status_$n'] == 'FAIL'){
				$output = array(
					'success' => false,
					'msg' => '退款失败'
				);
			}else if($result['refund_status_$n'] == 'PROCESSING'){
				$output = array(
					'success' => false,
					'msg' => '退款正在处理中'
				);
			}else if($result['refund_status_$n'] == 'NOTSURE'){
				$output = array(
					'success' => false,
					'msg' => '请重新发起退款'
				);
			}else if($result['refund_status_$n'] == 'CHANGE'){
				$output = array(
					'success' => false,
					'msg' => '接收退款的银行卡冻结了, 请联系客服'
				);
			}else{
				$output = array(
					'success' => false,
					'msg' => '未知错误, 请重新尝试'
				);
			}
		}else{
			$output = array(
				'success' => false,
				'msg' => '查询失败, 请重新尝试'
			);
		}
		
		return $output;
	}
	
	/**
	 * @name 生成成功提示
	 * @param string 提示语 
	 * @output JSON 
	 */
	public static function success($msg){
		
		echo json_encode(array(
			'return_code' => 'SUCCESS',
			'msg' => $msg
		));
	}
	
	/**
	 * @name 生成错误提示 
	 * @param string 提示语 
	 * @output JSON 
	 */
	public static function error($msg){
		
		echo json_encode(array(
			'return_code' => 'FAIL',
			'msg' => $msg
		));
	}

	//=======将XML转成数组=============//
	protected static function toArray($xml){
		//加上反斜杠防止引入命名空间时报错
		$xml_obj = new \SimpleXMLElement($xml);
		$xml_array = array();
		foreach($xml_obj as $key=>$value){
			//类型转换 
			$xml_array[$key] = (string)$value;
		}
		
		return $xml_array;
	}
	
	//======将数组转成XML==============//
	protected static function toXml($array){
		//\r\n是为了兼容windows notepad下的换行
		$xml = "<xml>\r\n";
		foreach($array as $key=>$value){
			$xml.="<$key>$value</$key>\r\n";
		}
		$xml.="</xml>\r\n";
		return $xml;
	}
	
	//=========生成签名算法===============//
	public static function makeSign($array){
		//排除sign字段
		unset($array['sign']);
		
		//字典排序
		ksort($array);
		//参数拼接，值为空不参与签名生成!!!
		foreach($array as $key=>$value){
			if($value != ''){
				$array[$key] = "$key=$value";
			}else{
				unset($array[$key]);
			}
		}
		
		//加上密钥 
		$array['key'] = "key=".self::$config['app_key']; 
		//连接成字符串
		$params = implode('&',$array);
		//得到签名
		return strtoupper(md5($params));
	}
	
	//========生成随机字串=======//
	public function makeNonceStr(){
		return base64_encode(time());
	}
	
	
	/**
	 * @name 使用证书发出POST请求 
	 * @param $arr array 参数 
	 * @param $url string 地址 
	 * @param $second integer 超时时间 
	 */
	public static function postSSL($url, $arr, $second = 30)
	{
		$xml = self::toXml($arr);
		$ch = curl_init();
		$config = self::$config;
		
		//超时时间
		curl_setopt($ch,CURLOPT_TIMEOUT, $second);
        curl_setopt($ch,CURLOPT_URL, $url);
        curl_setopt($ch,CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch,CURLOPT_SSL_VERIFYHOST, FALSE);
		//设置header
		curl_setopt($ch,CURLOPT_HEADER, FALSE);
		//要求结果为字符串且输出到屏幕上
		curl_setopt($ch,CURLOPT_RETURNTRANSFER, TRUE);
		//设置证书
		//使用证书：cert 与 key 分别属于两个.pem文件
		//默认格式为PEM，可以注释
		curl_setopt($ch,CURLOPT_SSLCERTTYPE, 'PEM');
		curl_setopt($ch,CURLOPT_SSLCERT, $config['SSLCERT_PATH']);
		//默认格式为PEM，可以注释
		curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
		curl_setopt($ch,CURLOPT_SSLKEY, $config['SSLKEY_PATH']);
		//post提交方式
		curl_setopt($ch,CURLOPT_POST, true);
		curl_setopt($ch,CURLOPT_POSTFIELDS, $xml);
		$result = curl_exec($ch);
		//返回结果
		if($result){
			curl_close($ch);
			return self::toArray($result);
		}
		else { 
			$error = curl_errno($ch);
			echo "curl出错，错误码:$error"."<br>"; 
			echo "<a href='http://curl.haxx.se/libcurl/c/libcurl-errors.html'>错误原因查询</a></br>";
			curl_close($ch);
			return false;
		}
	}
	
	/**
	 * @name 发出POST请求 
	 * @param $arr array 参数 
	 * @param $url string 地址 
	 * @param $second integer 超时时间 
	 * @return array 返回值 
	 */
	public static function post($url, $arr, $second = 30)
	{
		$xml = self::toXml($arr);
        //初始化curl        
       	$ch = curl_init();
		//设置超时
		curl_setopt($ch, CURLOP_TIMEOUT, $second);
        curl_setopt($ch,CURLOPT_URL, $url);
        curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
        curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);
		//设置header
		curl_setopt($ch, CURLOPT_HEADER, FALSE);
		//要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
		//post提交方式
		curl_setopt($ch, CURLOPT_POST, TRUE);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
		//运行curl
        $result = curl_exec($ch);
		
		//返回结果
		if($result)
		{
			curl_close($ch);
			return self::toArray($result);
		}
		else 
		{ 
			$error = curl_errno($ch);
			echo "curl出错，错误码:$error"."<br>"; 
			echo "<a href='http://curl.haxx.se/libcurl/c/libcurl-errors.html'>错误原因查询</a></br>";
			curl_close($ch);
			return false;
		}
	}
}


?>